/*
 * ========THE SOLMIX PROJECT=====================================
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.gnu.org/licenses/ 
 * or see the FSF site: http://www.fsf.org. 
 */

package org.solmix.web.security.realm;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.sql.DataSource;

import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.JdbcUtils;
import org.solmix.commons.logs.Logger;
import org.solmix.commons.util.Assert;
import org.solmix.fmk.util.ServiceUtil;
import org.solmix.services.exception.SLXException;
import org.solmix.sql.ConnectionManager;

/**
 * 
 * @author solomon
 * @version $Id$ 2011-6-5
 */

public class JdbcRealm extends AuthorizingRealm
{

   private static final Logger log = new Logger(JdbcRealm.class.getName());

   private String dataSourceFilter;

   protected DataSource dataSource;

   /**
    * The default query used to retrieve account data for the user.
    */
   protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from personnel where user_name  = ?";

   /**
    * The default query used to retrieve the roles that apply to a user.
    */
   protected static final String DEFAULT_USER_ROLES_QUERY = "select distinct role.role_id from personnel,role,role_attach where personnel.personnel_id=role_attach.role_attach_id and"
      +"role.role_id= role_attach.role_id and personnel.user_name = ?";

   /**
    * The default query used to retrieve permissions that apply to a particular role.
    */
   protected static final String DEFAULT_PERMISSIONS_QUERY = "select distinct right.right_id,right.right_flag,right.option from personnel,role,role_attach ,right,right_attach where "
      + " personnel.personnel_id=role_attach.role_attach_id and role.role_id= role_attach.role_id"
      +" and role.role_id=right_attach.role_id and right_attach.right_id=right.right_id and personnel.user_name = ?";

   protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;

   protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;

   protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;

   protected boolean permissionsLookupEnabled = false;

   public JdbcRealm()
   {
      this.setCredentialsMatcher(new CredentialsMatcher());
   }

   /**
    * @return the dataSourceFilter
    */
   public String getDataSourceFilter()
   {
      return dataSourceFilter;
   }

   /**
    * @param dataSourceFilter the dataSourceFilter to set
    */
   public void setDataSourceFilter(String dataSourceFilter)
   {
      this.dataSourceFilter = dataSourceFilter;
   }

   /**
    * {@inheritDoc}
    * 
    * @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
    */
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
   {
      if (principals == null)
      {
         throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
      }

      String username = (String) getAvailablePrincipal(principals);

      Connection conn = null;
      Set<String> roleNames = null;
      Set<String> permissions = null;
      try
      {
         if (dataSource == null)
            dataSource = (DataSource) ServiceUtil.getOsgiJndiService(DataSource.class.getName(), dataSourceFilter);
         conn = dataSource.getConnection();
         roleNames = getRoleNamesForUser(conn, username);
         if (permissionsLookupEnabled)
         {
            permissions = getPermissions(conn, username, roleNames);
         }
      } catch (SQLException e)
      {
         final String message = "There was a SQL error while authenticating user [" + username + "]";
         if (log.isErrorEnabled())
         {
            log.error(message, e);
         }

         // Rethrow any SQL errors as an authentication exception
         throw new AuthenticationException(message, e);
      } finally
      {
         try
         {
            ConnectionManager.freeConnection(conn);
         } catch (SLXException e)
         {
            throw new AuthenticationException(e.getMessage(), e);
         }

      }
      SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
      info.setStringPermissions(permissions);
      return info;
   }

   /**
    * @return the permissionsLookupEnabled
    */
   public boolean isPermissionsLookupEnabled()
   {
      return permissionsLookupEnabled;
   }

   /**
    * @param permissionsLookupEnabled the permissionsLookupEnabled to set
    */
   public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled)
   {
      this.permissionsLookupEnabled = permissionsLookupEnabled;
   }

   /**
    * @param conn
    * @param username
    * @param roleNames
    * @return
    */
   private Set<String> getPermissions(Connection conn, String username, Set<String> roleNames) throws SQLException
   {
      PreparedStatement ps = null;
      ResultSet rs = null;
      Set<String> permissions = new LinkedHashSet<String>();
      try {
          for (String roleName : roleNames) {

              ps = conn.prepareStatement(permissionsQuery);
              ps.setString(1, roleName);

              // Execute query
              rs = ps.executeQuery();

              // Loop over results and add each returned role to a set
              while (rs.next()) {
                 String permission =rs.getString(1);
                  String permissionOper = rs.getString(2);
                  String permissionIns = rs.getString(3);
                  if(Assert.isNotNullAndEmpty(permissionOper))
                     permission =permission+":"+permissionOper;
                  if(Assert.isNotNullAndEmpty(permissionIns))
                     permission =permission+":"+permissionIns;
                  // Add the permission to the set of permissions
                  permissions.add(permission);
              }

          }
      } finally {
          JdbcUtils.closeResultSet(rs);
          JdbcUtils.closeStatement(ps);
      }

      return permissions;
   }

   /**
    * @param conn
    * @param username
    * @return
    * @throws SQLException 
    */
   private Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException
   {
      PreparedStatement ps = null;
      ResultSet rs = null;
      Set<String> roleNames = new LinkedHashSet<String>();
      try {
          ps = conn.prepareStatement(userRolesQuery);
          ps.setString(1, username);

          // Execute query
          rs = ps.executeQuery();

          // Loop over results and add each returned role to a set
          while (rs.next()) {

              String roleName = rs.getString(1);

              // Add the role to the list of names if it isn't null
              if (roleName != null) {
                  roleNames.add(roleName);
              } else {
                  if (log.isWarnEnabled()) {
                      log.warn("Null role name found while retrieving role names for user [" + username + "]");
                  }
              }
          }
      } finally {
          JdbcUtils.closeResultSet(rs);
          JdbcUtils.closeStatement(ps);
      }
      return roleNames;
   }

   /**
    * {@inheritDoc}
    * 
    * @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
    */
   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
   {
      UsernamePasswordToken upToken = (UsernamePasswordToken) token;
      String username = upToken.getUsername();

      // Null username is invalid
      if (username == null)
      {
         throw new AccountException("Null usernames are not allowed by this realm.");
      }
      Connection conn = null;
      AuthenticationInfo info = null;
      try
      {
          if (dataSource == null)
          dataSource = (DataSource) ServiceUtil.getOsgiJndiService(DataSource.class.getName(), dataSourceFilter);
          conn = dataSource.getConnection();
          String password = getPasswordForUser(conn, username);
         if (password == null)
         {
            throw new UnknownAccountException("No account found for user [" + username + "]");
         }
         info = buildAuthenticationInfo(username, password.toCharArray());
      } catch (Exception e)
      {
         final String message = "There was a SQL error while authenticating user [" + username + "]";
         if (log.isErrorEnabled())
         {
            log.error(message, e);
         }

         // Rethrow any SQL errors as an authentication exception
         throw new AuthenticationException(message, e);
      } finally
      {
         try
         {
             ConnectionManager.freeConnection(conn);
         } catch (SLXException e)
         {
            throw new AuthenticationException(e.getMessage(), e);
         }

      }
      return info;
   }

   /**
    * @param username
    * @param charArray
    * @return
    */
   protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password)
   {
      return new SimpleAuthenticationInfo(username, password, getName());
   }

   /**
    * @param conn
    * @param username
    * @return
    * @throws SQLException 
    */
   protected String getPasswordForUser(Connection conn, String username) throws SQLException
   {
      PreparedStatement ps = null;
      ResultSet rs = null;
      String password = null;
      try {
          ps = conn.prepareStatement(authenticationQuery);
          ps.setString(1, username);

          // Execute query
          rs = ps.executeQuery();

          // Loop over results - although we are only expecting one result, since usernames should be unique
          boolean foundResult = false;
          while (rs.next()) {

              // Check to ensure only one row is processed
              if (foundResult) {
                  throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
              }

              password = rs.getString(1);

              foundResult = true;
          }
      } finally {
          JdbcUtils.closeResultSet(rs);
          JdbcUtils.closeStatement(ps);
      }

      return password;
   }

}
